#import "checkers_ai.h"
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>

#define CHECK_GRANULARITY 1024
#define OUT_OF_TIME -2000000

//Tune the AI function to implement distance heurestics ? close in when winning
//Trade when winning ? Check mobility ? overall more expensive
//Random noise seems bad, but ok for some randomness
static int chbHeuresticEvaluation(CheckersBoard* board) {
	int pos, noise;
	piece_t piece;
	int wsum, bsum, val, y;

	noise = 0;
	wsum = 0, bsum = 0;

	for (pos = 0; pos < BOARD_SQRS; pos++) {
		piece = board->board[pos];
		y = pos / BOARD_H;
		if (!piece)
			continue;
		if (abs(piece) == KING_PIECE) {
			val = 140;
		} else
			val = 100;
		/*TODO: Consider pieces near edges as less value ??*/
		if (y == 0 || y == 3){
			val -= 20;
		}
		if (piece > 0)
			wsum += val;
		else
			bsum += val;
	}
	if (board->random_noise){
		noise = (rand() * board->random_noise) / RAND_MAX - board->random_noise/2;
	}
	return  (board->iswhiteturn ?  wsum - bsum : bsum - wsum);
	/*
	 int sum = 0;
	 int nump = 0, numw = 0;
	 for (CheckerCoordinate[] cs : lookupCache){
	 for (CheckerCoordinate c : cs){
	 if (cb.isEmpty(c))
	 continue;
	 nump++;
	 int val = 0;
	 boolean iswhite = cb.isWhitePiece(c);
	 if (iswhite)
	 numw++;
	 if (cb.isKingPiece(c)){
	 val = 140;
	 } else {
	 val = 100;
	 int row = c.getRow();
	 int col = c.getColumn();
	 int drow = iswhite ? 7 - row: row;
	 int dcenter = col < 4 ? 3-col : col-4;
	 if (drow < 2 || drow == 7)
	 val += 20;
	 val -= dcenter * 2;
	 }
	 sum += iswhite ? val : -val;
	 }
	 }
	 return cb.isWhitesTurn() ? sum : -sum;*/
}

/*	The main AI function.
 *  This function implements a negamax algorithm for checkers.
 *  It returns a position evaluation, along with the best available move (stored in bestMove).
 * */
int chbEvaluatePosition(CheckersBoard* board, CheckersAISettings* ai,
		CheckersMove* bestMove, int depth, int alpha, int beta,
		BOOL extendCaptures) {
	BOOL waswhitesturn;
	int movecnt, i, eval, bestScore;
	CheckersMove moves[CHECKERS_MOVE_BUFFER];

	CheckersUnmove rollbackMove;
	CheckersMove* bestScoringMove = NULL;

	clock_t current_time;

	bestScore = -CHECKERS_WIN - 1;
	waswhitesturn = board->iswhiteturn;

	if ( (++ai->nodes) % CHECK_GRANULARITY == 0  ) {
		if (chbAITimedout(ai)) {
			return OUT_OF_TIME;
		}
	}
	if (depth <= 0 && !extendCaptures)
		return chbHeuresticEvaluation(board);

	movecnt = chbGenerateValidMoves(board, moves);

	if (movecnt == 0) {
		return -CHECKERS_WIN; //No moves left
	}

	if (depth <= 0 && (!extendCaptures || !board->mustjump)) {
		return chbHeuresticEvaluation(board);
	}

	for (i = 0; i < movecnt; i++) {
		//if move is cached, continue ... TODO
		//if move cached: continue;

		/*dont cut below depth 4*/
		if (board->random_branch_cuts){
			if ( i > 1 && board->random_branch_cuts > rand() * depth * depth /1000){
				/*Randomly forget to consider this move to simulate error*/
				continue;
			}
		}
		chbMakeMove(board, &moves[i], &rollbackMove);

		//if board occurred previously, return draw score ... TODO
		//		if (board in prev set) return draw

		if (board->isjumpseq)
			eval = chbEvaluatePosition(board, ai, NULL, depth,
				alpha, beta, board->mustjump);
		else eval = -chbEvaluatePosition(board, ai, NULL, depth - 1, -beta, -alpha,
				board->mustjump);

		/*Propogate end-of-time effects*/
		if (abs(eval) == OUT_OF_TIME) return OUT_OF_TIME;

		chbUnmakeMove(board, &rollbackMove);
		if (eval > bestScore) {
			bestScoringMove = &moves[i];
			bestScore = eval;
			if (bestScore > alpha) {
				alpha = bestScore;
				if (alpha >= beta) {
					//st = CheckerAIResult.SearchType.SCORE_LOWER_BOUND;
					break;
				}
			}
		}
	}
	if (bestMove && bestScoringMove)
		memcpy(bestMove, bestScoringMove, sizeof(CheckersMove));
	return bestScore;
}

void chbInitAISettings(CheckersBoard* notused, CheckersAISettings* ai) {
	ai->nodes = 0;
	ai->time_limit = 10;
	ai->random_branch_cuts = 0;
	ai->random_noise = 0;
	ai->timeout = 0;
	ai->depth_limit = 7;
}

BOOL chbAITimedout(CheckersAISettings* ai) {
	return ai->timeout < clock();
}
void chbSetAITimeout(CheckersAISettings* ai) {
	ai->timeout = clock() + ai->time_limit;
}

int chbComputeAIResponse(CheckersBoard* board, CheckersAISettings* ai,
		CheckersMove* bestMove) {

	/*Uses buffers for each iteration - time outs will cause data corruption*/
	CheckersBoard boardbuff;
	CheckersMove movebuff;
	int i, eval, valid_eval = OUT_OF_TIME;
	chbSetAITimeout(ai);

	/*Cutting branches introduces a random chance of completely ignoring a move possibility*/
	board->random_branch_cuts = ai->random_branch_cuts;
	board->random_noise = ai->random_noise;

	for (i = 1; i <= ai->depth_limit; i++){
		memcpy(&boardbuff, board, sizeof(CheckersBoard));
		/*In case of time-out, eval and movebuff will be filled with non-sense*/
		eval = chbEvaluatePosition(&boardbuff, ai, &movebuff, (i),
			-CHECKERS_WIN - 1, CHECKERS_WIN + 1, NO);
		if (!chbAITimedout(ai)){
			memcpy(bestMove, &movebuff, sizeof(CheckersMove));
			valid_eval = eval;
		} else break;
	}
    CheckersUnmove unmove;
    printf("Generated move %d %d %d %d\n", bestMove->from.x, bestMove->from.y,
           bestMove->to.x, bestMove->to.y);
	printf("Completed depth %d search\n", i - 1); //- (chbAITimedout(ai)));
	return valid_eval;
}
/*
 *
 *
 if ( previousBoardSet.contains(node))
 return 0;

 nodesSearched++;
 if ( (nodesSearched % nodesPerTimeCheck) == 0){
 if (System.currentTimeMillis() >= finalTime)
 throw new CheckerAIOutOfTime("Method has run out of time, exiting abruptly.");
 }
 if (depth <= 0 && !extendCaptures)
 return heuristicEvaluation(node);
 boolean isjmpsq = node.isJumpSequence();
 CheckerMove[] moves = getSortedMoves(node);
 if (moves.length == 0)
 return -WIN;
 if (depth <= 0 && extendCaptures && !moves[0].isJump())
 return heuristicEvaluation(node);

 MyHashMapEntry<CheckerBoard, CheckerAIResult> mhpe = null;
 CheckerAIResult cachedResult = null;
 CheckerMove cachedMove = null;
 CheckerAIResult.SearchType st = CheckerAIResult.SearchType.SCORE_EXACT;

 //Don't read the cache in jump sequences
 if (!isjmpsq && depth >= MINCACHEDEPTH){
 mhpe = resultCache.get(node);
 }
 int bestScore = Integer.MIN_VALUE;
 CheckerMove bm = null;

 //Get a value from the cache
 if (mhpe != null){
 cachedResult = mhpe.getValue();
 cachedMove = cachedResult.getBestMove();
 bm = cachedMove;
 bestmove = bm;
 if (cachedResult.getSearchDepth() >= depth){
 bestScore = cachedResult.getBestScore();//Temporary, might not be valid
 switch (cachedResult.getSearchType()){
 case SCORE_EXACT:
 return bestScore;
 case SCORE_LOWER_BOUND:
 if (bestScore > max){
 max = bestScore;
 if (max >= min)
 return max;
 }
 break;
 case SCORE_UPPER_BOUND:
 if (bestScore < min){
 min = bestScore;
 if (max >= min)
 return max;
 }
 bestScore = Integer.MIN_VALUE;//Wasn't actually valid value here, correct this
 }
 }
 }
 for (int i = (cachedMove != null ? -1 : 0); //Start at -1 if we have a cached move to try
 i < moves.length;
 i++){
 //Try the cached move first
 CheckerMove move = (i == -1 ? cachedMove : moves[i]);
 if (i > -1 && move.equals(cachedMove))
 continue;
 boolean isJump = move.isJump();
 CheckerBoard child = move.getBoardAfter();
 int eval;
 //Continue jump sequence, don't decrease depth
 //Dont worry about board sets during a jump sequence
 if (!node.isJumpSequence())
 previousBoardSet.add(node);
 if (child.isWhitesTurn() == node.isWhitesTurn()){
 eval = analyze(child,depth,max,min,isJump);
 }
 else
 eval = -analyze(child,depth - 1, -min, -max, isJump);
 if (!node.isJumpSequence())
 previousBoardSet.remove(node);
 if (eval > bestScore){
 bm = move;
 bestScore = eval;
 if (bestScore > max){
 max = bestScore;
 if (max >= min){
 st = CheckerAIResult.SearchType.SCORE_LOWER_BOUND;
 break;
 }
 }
 }
 }
 if (bestScore < max)
 st = CheckerAIResult.SearchType.SCORE_UPPER_BOUND;
 //Add to cache ...
 if (!isjmpsq && depth >= MINCACHEDEPTH && resultCache.size() < CACHEMAX){
 CheckerAIResult cair = new CheckerAIResult(bestScore, bm, depth, st);
 if (cachedResult == null || cachedResult.getSearchDepth() <= depth)
 resultCache.put(node, cair);
 }

 bestmove = bm;//Sneaky way of returning an extra value
 return bestScore;
 */
